fix(gsd): prevent auto-mode setModel calls from persisting to settings.json#3486
fix(gsd): prevent auto-mode setModel calls from persisting to settings.json#3486deseltrus wants to merge 5 commits intogsd-build:mainfrom
Conversation
🔴 PR Risk Report — CRITICAL
Affected Systems
File Breakdown
|
Adversarial Review —
|
|
This PR has merge conflicts with the base branch. Please rebase or merge 🤖 Automated PR audit — 2026-04-04 |
f00cff5 to
46cf305
Compare
|
Rebased this branch onto current What changed in the refresh:
Local verification on the refreshed branch:
CI should now be running on the cleaned branch head. |
46cf305 to
cfaee54
Compare
|
Addressed the adversarial review finding — good catch. Root cause: Fix: Moved Added regression test: Structural assertion verifying All 4 model-switch tests + 13 model-isolation tests pass. Branch rebased on latest main. |
trek-e
left a comment
There was a problem hiding this comment.
No linked issue — please open a GitHub issue for this bug and add 'Closes #NNN' to the PR body.
The fix is thorough and addresses all three layers correctly:
-
loader.ts — the root cause fix. The ExtensionAPI setModel wrapper was silently dropping the options argument, rendering every downstream persist guard ineffective. The one-line fix is correct.
-
auto.ts — the two setModel calls (stopAuto model restore and dispatchHookUnit hook model) now pass { persist: false }. Correct.
-
agent-session.ts — _applyThinkingLevel now gates both appendThinkingLevelChange and setDefaultThinkingLevel behind the persist check. The structural test (appendIdx > persistGuardIdx) verifies the ordering is correct and will catch any future regression that moves the append call outside the guard.
The package-lock.json version bump from 2.56.0 to 2.63.0 appears unrelated to the fix. If this was an accidental rebase artifact, it should be reverted to avoid noise in the diff. If it is intentional, it should be called out in the PR description.
Everything else — the structural guard test in model-isolation.test.ts, the loader test, the thinking-level session history test — is exactly the right coverage for this kind of silent persistence bug.
The ExtensionAPI.setModel wrapper in loader.ts accepts only the model
parameter, silently dropping the options argument. This means every
pi.setModel(model, { persist: false }) call from any extension becomes
runtime.setModel(model) — the persist guard is lost and transient model
switches always overwrite the user's saved default in settings.json.
_applyModelChange() calls setThinkingLevel() unconditionally, which
always writes the effective thinking level to settings.json. When
auto-mode dispatches use setModel({ persist: false }), the model
default is correctly protected, but the thinking level default is not.
If a transient model switch triggers thinking level clamping (e.g.
xhigh → high), the clamped level is persisted and every subsequent
session starts with the wrong thinking level.
Extract _applyThinkingLevel(level, options?) as a private method that
respects the persist option from _applyModelChange. The public
setThinkingLevel(level) API is unchanged — it delegates to
_applyThinkingLevel without options, preserving the existing persist
behavior for user-initiated changes.
…s.json
Two setModel() calls in auto.ts omit { persist: false }, causing
transient model switches during auto-mode to silently overwrite the
user's saved default model in settings.json. Every subsequent session
then starts with the wrong model.
Adds a structural regression test that fails if any setModel call in
auto.ts is missing persist: false.
Add targeted structural regressions for the two persistence leaks behind the auto-mode settings overwrite bug. The new tests pin the ExtensionAPI setModel wrapper to forward options and assert that transient model switches do not persist thinking-level defaults when persist:false is used.
Address review feedback: appendThinkingLevelChange() was recording durable entries even for transient (persist:false) model switches. On session resume, replay would call the public setThinkingLevel() which always persists — silently overwriting the user's saved default. Move appendThinkingLevelChange inside the persist guard so transient thinking-level changes leave no trace in session history. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
cfaee54 to
ebe61b7
Compare
trek-e
left a comment
There was a problem hiding this comment.
Prior feedback fully addressed. LGTM.
- Linked issue added (Closes #4339). Done.
- package-lock.json version bump artifact is no longer present in the diff. Done.
- All three fix layers are correct (loader.ts options forwarding, auto.ts persist:false calls, agent-session.ts thinking-level guard).
- Regression tests cover all the important seams.
- Full CI is green (build, lint, integration-tests, windows-portability, all rtk-portability variants — all passing as of 2026-04-16).
trek-e
left a comment
There was a problem hiding this comment.
Verdict: REQUEST_CHANGES | Severity: MAJOR
The three-layer fix correctly addresses the stated problem. However there is one real bug and one structural test weakness that need resolution before merge.
MAJOR — _emitSessionStateChanged("set_thinking_level") fires even on no-op persist:false paths
In the new _applyThinkingLevel, _emitSessionStateChanged("set_thinking_level") sits outside the if (options?.persist !== false) guard but still inside if (isChanging). When called with persist: false, the event fires and any subscribers (UI, telemetry) will observe a thinking-level-changed signal even though no durable change was made. Whether this is a regression depends on pre-existing behavior — but the intent of persist: false is "transient, leave no trace", and an emitted session-state event is a trace. This should either be gated behind the same persist check or explicitly documented as intentional.
MAJOR — structural test for auto.ts is bypassable with multiline calls
tests/model-isolation.test.ts uses:
const setModelCalls = autoSrc.match(/\.setModel\([^)]*\)/g) ?? [];[^)]* does not span newlines, so any setModel call formatted across multiple lines:
await pi.setModel(
match,
{ persist: false },
);produces \.setModel( with no closing ) in the match, yielding an empty array or a truncated match that fails to include persist: false. The test would then silently pass (the call isn't matched) or spuriously fail. This is the enforcement guarantee for the entire PR — if it can be bypassed by a formatting change the guard is hollow.
Fix: use a multiline-aware regex or read the AST. At minimum, use /\.setModel\([\s\S]*?\)/g with care for nested parens.
MINOR — setThinkingLevel public method is now a thin wrapper with no independent tests
The public API contract (setThinkingLevel always persists) is implicit — it just calls _applyThinkingLevel(level) with no options, so options?.persist !== false is true. This is correct, but it's worth a single explicit test asserting that calling the public method writes to settingsManager. Currently the tests only probe source text structure, not runtime behavior.
What's correct:
loader.tsoptions forwarding is minimal and obviously right.auto.tscall sites — bothstopAutoanddispatchHookUnit— are correctly updated.appendThinkingLevelChangebeing inside the persist guard is the right fix for the session-resume re-persistence loop described in the PR.- The comment on
_applyModelChange's existingpersist !== falseguard forsetDefaultModelAndProviderconfirms this PR is consistent with the prior guard pattern.
Problem
Auto-mode temporarily switches models for hook dispatch and stop/restore operations. These transient
setModelcalls persist the model choice tosettings.json, silently overwriting the user's default model preference.Root cause:
ExtensionAPI.setModel()inloader.tsdrops theoptionsargument, so{ persist: false }never reaches the session layer.Secondary leak:
_applyThinkingLevel()unconditionally appendsthinking_level_changeto session history, which on resume re-persists viasetThinkingLevel().Closes #4339
Fix
Three layers:
loader.ts— forwardoptionsthroughsetModelwrapperauto.ts— pass{ persist: false }for all transient model switchesagent-session.ts— gateappendThinkingLevelChange()andsetDefaultThinkingLevel()behind persist checkTests
setModelcalls inauto.tsmust includepersist: falseloader.ts: options forwarding unit testagent-session.ts: thinking-level history entry gated by persist optionappendThinkingLevelChangeindex vs persist-guard index ordering test